﻿using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.ExceptionHandling;
using Volo.Abp.Threading;

namespace Xms.Abp.Threading;

public class AsyncCronTimer : ITransientDependency
{
    /// <summary>
    /// This func is raised periodically according to Period of Timer.
    /// </summary>
    public Func<AsyncCronTimer, Task> Elapsed = _ => Task.CompletedTask;

    private Cron _Cron;
    private string _CronExpression = "0 0 0 31 2 ?";

    public string CronExpression
    {
        get => _CronExpression;
        set
        {
            _Cron = Cron.Parse(value);
            _CronExpression = value;
            if (_isRunning)
            {
                Stop();
                Start();
            }
        }
    }

    /// <summary>
    /// Indicates whether timer raises Elapsed event on Start method of Timer for once.
    /// Default: False.
    /// </summary>
    public bool RunOnStart { get; set; }

    public ILogger<AsyncCronTimer> Logger { get; set; }

    public IExceptionNotifier ExceptionNotifier { get; set; }

    private readonly Timer _taskTimer;
    private volatile bool _performingTasks;
    private volatile bool _isRunning;

    public Action OnExitRunning { get; set; }

    public bool IsRunning => _isRunning;

    public AsyncCronTimer()
    {
        ExceptionNotifier = NullExceptionNotifier.Instance;
        Logger = NullLogger<AsyncCronTimer>.Instance;

        _Cron = Cron.Parse(_CronExpression);

        _taskTimer = new Timer(
            TimerCallBack,
            null,
            Timeout.Infinite,
            Timeout.Infinite
        );
    }

    public void Start()
    {
        if (_isRunning)
        {
            return;
        }
        var nextExecuteTime = GetNextOccurrence();
        if (!nextExecuteTime.HasValue)
        {
            OnExitRunning?.Invoke();
            return;
        }
        var period = (int)(nextExecuteTime.Value - DateTime.Now).TotalMilliseconds;
        //if (period <= 0)
        //{
        //    throw new AbpException("Period should be set before starting the timer!");
        //}

        lock (_taskTimer)
        {
            _taskTimer.Change(RunOnStart ? 0 : period, Timeout.Infinite);
            _isRunning = true;
        }
    }

    public void Stop()
    {
        if (!_isRunning)
        {
            return;
        }
        lock (_taskTimer)
        {
            _taskTimer.Change(Timeout.Infinite, Timeout.Infinite);
            while (_performingTasks)
            {
                Monitor.Wait(_taskTimer);
            }

            _isRunning = false;
        }
    }

    /// <summary>
    /// This method is called by _taskTimer.
    /// </summary>
    /// <param name="state">Not used argument</param>
    private void TimerCallBack(object state)
    {
        lock (_taskTimer)
        {
            if (!_isRunning || _performingTasks)
            {
                return;
            }

            _taskTimer.Change(Timeout.Infinite, Timeout.Infinite);
            _performingTasks = true;
        }

        AsyncHelper.RunSync(async () => await Timer_Elapsed());
    }

    private async Task Timer_Elapsed()
    {
        try
        {
            await Elapsed(this);
        }
        catch (Exception ex)
        {
            Logger.LogException(ex);
            await ExceptionNotifier.NotifyAsync(ex);
        }
        finally
        {
            lock (_taskTimer)
            {
                _performingTasks = false;
                if (_isRunning)
                {
                    var nextExecuteTime = GetNextOccurrence();
                    if (nextExecuteTime.HasValue)
                    {
                        var period = (int)(nextExecuteTime.Value - DateTime.Now).TotalMilliseconds;
                        _taskTimer.Change(period, Timeout.Infinite);
                    }
                    else
                    {
                        _isRunning = false;
                        OnExitRunning?.Invoke();
                    }
                }

                Monitor.Pulse(_taskTimer);
            }
        }
    }

    public DateTime? GetNextOccurrence()
    {
        var now = DateTime.Now.AddSeconds(1).AddMilliseconds(-1).ToUniversalTime().AddHours(8);
        var resultUtc = _Cron.GetNextOccurrence(now);

        if (resultUtc.HasValue)
        {
            return resultUtc.Value.ToLocalTime().AddHours(-8);
        }
        return null;
    }
}

